package com.hmall.item.es;

import cn.hutool.core.bean.BeanUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.NestedAggregate;
import co.elastic.clients.elasticsearch._types.aggregations.StatsAggregate;
import co.elastic.clients.elasticsearch._types.query_dsl.FunctionBoostMode;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import co.elastic.clients.util.NamedValue;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.hmall.common.utils.BeanUtils;
import com.hmall.item.domain.po.Item;
import com.hmall.item.domain.po.ItemDoc;
import com.hmall.item.service.IItemService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author Mr.M
 * @version 1.0
 * @description 使用Java Client 操作ES
 * @date 2024/8/18 17:40
 */
@SpringBootTest
@Slf4j
public class IndexTest {

    private ElasticsearchClient esClient;

    private RestClient restClient;

    @Autowired
    private IItemService iItemService;

    @BeforeEach
    void setUp() {
        // Create the low-level client
        this.restClient = RestClient.builder(
                new HttpHost("192.168.101.68", 9200)).build();

        // 创建 ObjectMapper 实例
        ObjectMapper objectMapper = new ObjectMapper();
        // 添加 JavaTimeModule 以支持 LocalDateTime 类型
        objectMapper.registerModule(new JavaTimeModule());

        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper(objectMapper));


        // And create the API client
        this.esClient = new ElasticsearchClient(transport);
    }

    //创建文档

    @Test
    void testAddDocument() throws IOException {
        //1.根据id查询商品数据
        Item item = iItemService.getById(100002644680L);
        //2.转换为文档类型
        ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
        IndexResponse response = esClient.index(i -> i
                .index("items")//指定索引名称
                .id(itemDoc.getId())//指定主键
                .document(itemDoc)//指定文档对象
        );
        //结果
        String s = response.result().jsonValue();
        log.info("result:" + s);
    }

    ;


    @Test
    void testGetDocumentById() throws IOException {
        GetResponse<ItemDoc> response = esClient.get(g -> g
                        .index("items")
                        .id("100002644680"),
                ItemDoc.class);

        if (response.found()) {
            ItemDoc itemDoc = response.source();
            log.info("itemDoc: " + itemDoc);
        } else {
            log.info("itemDoc not found");
        }
    }


    @Test
    void testDelete() throws IOException {
        DeleteResponse response = esClient.delete(d -> d
                .index("items")
                .id("100002644680")
        );
        String s = response.result().jsonValue();
        log.info("result:" + s);
    }


    @Test
    void testUpdateDocumentById() throws IOException {
        //更新对象
        ItemDoc itemDoc = new ItemDoc();
        itemDoc.setName("更新名称");
        UpdateResponse<ItemDoc> response = esClient.update(up -> up
                .index("items")
                .id("100002644680")
                .doc(itemDoc), ItemDoc.class);
        String s = response.result().jsonValue();
        log.info("result:" + s);
    }


    @Test
    void testTerm() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.term(t -> t.field("category").value("拉杆箱")))
                .build();

        SearchResponse<ItemDoc> searchResponse = esClient.search(request, ItemDoc.class);

        long total = searchResponse.hits().total().value();
        log.info("查询到{}条数据", total);
        //解析结果
        List<Hit<ItemDoc>> hits = searchResponse.hits().hits();
        //遍历
        hits.forEach(hit -> {
            ItemDoc source = hit.source();
            log.info("查询到数据：{}", source);
        });
    }

    //range范围查询编写


    //复合查询bool查询 must与，should或，must_not非

    @Test
    void testBool() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.bool(b -> b
                        .must(m -> m.match(m1 -> m1.field("name").query("绿色拉杆箱")))
                        .should(s -> s.term(s1 -> s1.field("brand").value("汉客")))
                        .should(s -> s.term(s1 -> s1.field("brand").value("爱华仕")))
                        .minimumShouldMatch("1")
                        .filter(f -> f.range(r -> r.field("price").gte(JsonData.of(3000)).lte(JsonData.of(10000))))
                ))
                .build();

        SearchResponse<ItemDoc> searchResponse = esClient.search(request, ItemDoc.class);

        handleResponse(searchResponse);

    }

    private void handleResponse(SearchResponse<ItemDoc> response) {
        //获取总条数
        Long total = response.hits().total().value();
        log.info("查到{}条数据", total);
        //解析结果
        List<Hit<ItemDoc>> hits = response.hits().hits();
        //遍历hits
        hits.forEach(hit -> {
            ItemDoc source = hit.source();
            log.info("数据为{}", source);
        });
    }


    @Test
    void testDuLiangSearch() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.bool(b -> b
                        .filter(f -> f.term(t -> t.field("category").value("手机")))
                        .filter(f -> f.range(r -> r.field("price").gte(JsonData.of(1000))))
                ))
                .size(0)
                .aggregations("brand_agg", a -> a.terms(t -> t.field("brand")
                        .size(20)
                        .order(NamedValue.of("price_stats.avg", SortOrder.Desc))
                ))
                .aggregations("price_stats", a -> a.stats(s -> s.field("price")))
                .build();

        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);

        //解析出聚合结果
        Aggregate brandAgg = response.aggregations().get("brand_agg");
        brandAgg.sterms().buckets().array().forEach(bucket -> {
            FieldValue key = bucket.key();
            long docCount = bucket.docCount();
            StatsAggregate priceStats = bucket.aggregations().get("price_stats").stats();
            double avg = priceStats.avg();
            double min = priceStats.min();
            double max = priceStats.max();
            log.info("品牌:{},商品数量:{},平均价格:{},最大价格:{},最小价格:{}", key, docCount, avg, max, min);
        });

    }

    //复合查询
    @Test
    void testBoll() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.bool(b -> b.must(m -> m.match(m1 -> m1.field("name").query("绿色拉杆箱")))
                        .should(s -> s.term(t -> t.field("brand").value("汉客")))
                        .should(s -> s.term(t -> t.field("brand").value("爱华仕")))
                        .minimumShouldMatch("1")
                        .filter(f -> f.range(r -> r.field("price").gte(JsonData.of(3000)).lte(JsonData.of(10000))))
                ))
                .build();
        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
        handleResponse(response);
    }


    @Test
    void testBatchAddDocment() throws Exception {
        int number = 0;
        int size = 1000;
        while (true) {
            //取第一页10条数据
            Page<Item> page = Page.of(number, size);
            //查询所有商品信息
            Page<Item> itemPage = iItemService.page(page, new LambdaQueryWrapper<Item>());
            //获取商品集合
            List<Item> items = itemPage.getRecords();
            if (items.isEmpty()) {
                break;
            }
            //拷贝属性
            List<ItemDoc> itemDocs = BeanUtils.copyList(items, ItemDoc.class);
            //批量添加请求
            BulkRequest.Builder br = new BulkRequest.Builder();
            itemDocs.forEach(itemDoc ->
                    br.operations(op -> op
                            .index(i -> i
                                    .index("items")
                                    .id(itemDoc.getId().toString())
                                    .document(itemDoc))));
            //构建请求
            BulkRequest build = br.build();
            //批量添加
            BulkResponse bulkResponse = esClient.bulk(build);
            //遍历结果
            bulkResponse.items().forEach(item -> log.info("添加结果：{}", item.result().toString()));
            //如果有错误
            if (bulkResponse.errors()) {
                log.error("批量添加失败");
                //遍历错误
                bulkResponse.items().forEach(item -> log.error("添加失败：{}", item.error().reason()));
            }
            number++;
        }
    }


    @Test
    void testGuoLv() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.functionScore(f -> f
                        //原始查询条件，这里先以match方式查询field字段，然后query具体的内容
                        .query(q1 -> q1.match(m -> m.field("name").query("诺基亚手机")))
                        .functions(fu -> fu
                                //过滤条件
                                .filter(f1 -> f1.term(t -> t.field("brand").value("小米")))
                                //算分函数，这里是乘10
                                .weight(10d)
                        )
                        //将小米的权重分数以乘法方式运算
                        .boostMode(FunctionBoostMode.Multiply)
                ))
                .build();

        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
        //解析结果
        handleResponse(response);
    }


    @Test
    void testSearchAfter() throws IOException {
        // 1.创建Request
        SearchRequest.Builder builder = new SearchRequest.Builder();
        builder.index("items");

        builder.query(q -> q
                        .bool(b -> b))
                .sort(s1 -> s1
                        .field(f -> f.field("id").order(SortOrder.Asc)))
                .searchAfter("0")
                .size(1);
        SearchRequest request = builder.build();
        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
        // 解析响应
        handleResponse(response);

    }


    // 按照地理位置进行搜索
    @Test
    void testDiLi() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.geoBoundingBox(gbb -> gbb
                        .field("location").boundingBox(bb -> bb.tlbr(tl -> tl
                                .topLeft(tt -> tt.latlon(ll -> ll.lat(34.37).lon(108.87)))
                                .bottomRight(br -> br.latlon(ll -> ll.lat(34.36).lon(108.90)))
                        ))))
                .build();
        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
        handleResponse(response);
    }


    /**
     * 批量导入文档-补充商品属性
     */
    @Test
    void testBulk2() throws IOException {
        int pageNum = 1;
        int pageSize = 100;
        //分页从数据库中查询数据
        IPage<Item> page = new Page<>(pageNum, pageSize);
        iItemService.page(page);
        List<Item> records = page.getRecords();

        //将List<Item>  ->  List<ItemDoc>
        List<ItemDoc> itemDocs = BeanUtils.copyList(records, ItemDoc.class);

        //批量导入到es中
        BulkRequest.Builder br = new BulkRequest.Builder();

        //遍历itemDocs数据集，循环将文档添加到bulk中
        itemDocs.forEach(itemDoc ->
                {
                    //设置商品属性
                    List<ItemDoc.Spec> specs = new ArrayList<>();

                    //给商品设置第一个属性
                    ItemDoc.Spec spec1 = new ItemDoc.Spec();
                    spec1.setName("颜色");
                    spec1.setValue("黑色");
                    specs.add(spec1);

                    ItemDoc.Spec spec2 = new ItemDoc.Spec();
                    spec2.setName("尺码");
                    spec2.setValue("X");
                    specs.add(spec2);

                    itemDoc.setAttr_list(specs);

                    br.operations(op ->
                            op.index(i -> i
                                    .index("items")
                                    .id(itemDoc.getId().toString())
                                    .document(itemDoc)
                            ));
                }
        );

        BulkRequest request = br.build();
        //执行导入操作
        BulkResponse response = esClient.bulk(request);

        log.info("第{}批量导入结果：{}", pageNum, response.errors());
        log.info("第{}批量导入数量：{}", pageNum, response.items().size());

    }

    //查询尺码xl
    @Test
    void testNested() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .query(q -> q.nested(n -> n.path("attr_list").query(q1 -> q1.bool(
                        b -> b.must(m -> m.term(t -> t.field("attr_list.name").value("尺码")))
                                .must(m -> m.term(t -> t.field("attr_list.value").value("XL")))
                ))))
                .build();
        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);

        handleResponse(response);
    }


    //先按商品属性名称聚合，再按属性值聚合。


    @Test
    void testNested1() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("items")
                .size(0)
                .aggregations("attr_aggs", a -> a.nested(an -> an.path("attr_list"))
                        .aggregations("attr_name_aggs", a1 -> a1.terms(t -> t.field("attr_list.name")
                                                .size(10)
                                        )
                                        .aggregations("attr_value_aggs", a2 -> a2.terms(t -> t.field("attr_list.value")
                                                .size(10)))
                        )
                )
                .build();

        SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
        Map<String, Aggregate> aggregations = response.aggregations();
        Aggregate attrAggs = aggregations.get("attr_aggs");
        //解析结果
        NestedAggregate nested = attrAggs.nested();
        Map<String, Aggregate> attrNameAggs = nested.aggregations();
        Aggregate aggregate = attrNameAggs.get("attr_name_aggs");
        aggregate.sterms().buckets().array().forEach(bucket -> {
            String key = bucket.key().stringValue();
            Long docCount = bucket.docCount();
            log.info("属性名:{},属性值数量:{}", key, docCount);
            Map<String, Aggregate> aggregations1 = bucket.aggregations();
            Aggregate attrValueAggs = aggregations1.get("attr_value_aggs");
            attrValueAggs.sterms().buckets().array().forEach(bucket1 -> {
                String key1 = bucket1.key().stringValue();
                Long docCount1 = bucket1.docCount();
                log.info("属性值:{},属性值数量:{}", key1, docCount1);
            });
        });
    }


    //自动补全查询

    @Test
    void testSuggest() throws IOException {
        SearchRequest request = new SearchRequest.Builder()
                .index("test_index2")
                .suggest(s -> s.suggesters("suggestion_suggest", ss -> ss.completion(c -> c
                                        .field("suggestion")
                                        .size(10)
                                        .skipDuplicates(true)//去重
                                )
                                .text("拉")
                ))
                .build();

        SearchResponse<Index2> response = esClient.search(request, Index2.class);

        Map<String, List<Suggestion<Index2>>> suggest = response.suggest();
        List<Suggestion<Index2>> suggestionSuggest = suggest.get("suggestion_suggest");

        suggestionSuggest.forEach(suggestion -> {
            suggestion.completion().options().forEach(option -> {
                String text = option.text();
                System.out.println(text);
            });
        });
    }





    /**
     * 测试自动补全模型类
     */
    @Data
    public static class Index2 {
        //id
        private Long id;
        //name
        private String name;
        private List<String> suggestion;

    }


    @AfterEach
    void tearDown() throws IOException {
        this.restClient.close();
    }

}